package Library;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;



//import Library.filesystem.FLO;
//import Library.filesystem.FileSystem;
//import Library.filesystem.IDGenerator;
import Library.DeviceManager.DeviceAlreadyExistsExcept;
import agg.HexTools;

public class Device implements DeviceInterface {
	/**
	 * The type of Device. Generally tells what kind of data the Device is
	 * designed to store. 
	 */
	public static String DEVICE_TYPE = "generic";
	
	/**
	 * If this is True, a user may create this device from the UI.
	 */
	public boolean accessible = false;
	
	/**
	 * A connection to the local SQL database.
	 */
	private Connection conn;

	/**
	 * The folder in which to store all database files.
	 */
	private static String dbFolder = "bin/db/";

	/**
	 * The String with which to prefix the database name.
	 */
	private String dbPrefix = "device_";

	/**
	 * The device name.
	 */
	private String name = "";

	/**
	 * A map of all table names, to their creation Strings.
	 */
	private ConcurrentHashMap<String, String> tables = 
		new ConcurrentHashMap<String, String>();

	/**
	 * Device Properties, a map with keys as the property name and values as the
	 * property value as a String.
	 */
	private ConcurrentHashMap<String, String> properties = 
		new ConcurrentHashMap<String, String>();
	
	/**
	 * All DeviceItems contained in this Device.
	 */
	private ConcurrentHashMap<String, DeviceItem> items = 
		new ConcurrentHashMap<String, DeviceItem>();

	/**
	 * Create a Device with the given name.
	 * @param deviceName what to name this device.
	 * @throws SQLException thrown on SQL error when creating/loading db/tables.
	 */
	public Device(String deviceName) 
	throws SQLException {
		name = deviceName;

		// Load the HSQL Database Engine JDBC driver
		// NOTE :: hsqldb.jar is required and located in /lib.
		// NOTE :: not sure why this is needed, but it's used in example code...
		try {
			Class.forName("org.hsqldb.jdbcDriver");
		} catch (ClassNotFoundException e) {
			// this shouldn't happen...
			e.printStackTrace();
		}

		// Create a JDBC connection to the stand-alone hsqldb server.
		conn = DriverManager.getConnection(
				"jdbc:hsqldb:file:" + dbFolder + dbPrefix + name,
				"sa", "");

		populateTables();
		setDefaultProperties();
		
		// TODO get all saved items and load into the 'items' HashMap
	}
	
	/**
	 * A Device instance for registration purposes.
	 */
	public Device() {}
	
	/**
	 * Create and return a new instance of this Device, with the given name.
	 * @return the new device instance.
	 * @throws SQLException 
	 */
	public DeviceInterface getNewDeviceInstance(String name) throws SQLException {
		return new Device(name);
	}
	
	/**
	 * Register this Device with the DeviceManager.
	 */
	public static void registerDevice() {
		Device r = new Device();
		DeviceManager.registerDeviceType(DEVICE_TYPE, r);
	}

	/**
	 * Should be called when the program is ended to perform a complete and
	 * proper shutdown of the SQL database.
	 * @throws SQLException
	 */
	public synchronized void shutdown() throws SQLException {
		if ( !conn.isClosed() ) {
			Statement st = conn.createStatement();
			if ( !conn.getAutoCommit() ) conn.commit();
			st.execute("SHUTDOWN;"); //+ "COMPACT;");
			conn.close();
		}
	}
	
	public void addItem(DeviceItem n) throws SQLException {
		if ( !items.containsKey(name) ) {
			items.put(name, n);
			
			// serialize item
			ByteArrayOutputStream bArr = new ByteArrayOutputStream();
			ObjectOutputStream out = null;
			try {
				out = new ObjectOutputStream(bArr);
				out.writeObject(n);
				out.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			String hexBuffer = HexTools.bufferToHex(bArr.toByteArray());
			
			// add to SQL
			StringBuilder q = new StringBuilder();
			q.append("INSERT INTO DEVICE_ITEMS(value) VALUES('");
			q.append(hexBuffer).append("')");
			update(q);
		}
	}
	
	public ConcurrentHashMap<String, DeviceItem> getItems() {
		return items;
	}
	
	/**
	 * Return the Device's name
	 * @return the device name
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get the value of the given propertyName. If the property does not exist,
	 * null is returned. 
	 * @param propertyName
	 * @return
	 */
	public String getProperty(String propertyName) {
		if ( properties.containsKey(propertyName) ) {
			return properties.get(propertyName);
		}
		return null;
	}

	/**
	 * Get the value of the given propertyName. If the property does not exist
	 * or is empty, the defaultValue will be returned instead.
	 * @param propertyName
	 * @param defaultValue
	 * @return
	 * @throws SQLException 
	 */
	public String getProperty(String propertyName, String defaultValue) 
	throws SQLException {
		// try to get the property from the local memory first
		if ( properties.containsKey(propertyName) ) {
			return properties.get(propertyName);
		}
		else {
			// now we check if the value exists in the database
			if ( checkPropertyExists(propertyName) ) {
				// it exists, so get value from DB
				String val = "";

				StringBuilder q = new StringBuilder("");
				q.append("SELECT * FROM DEVICE_PROPERTIES WHERE property='");
				q.append(sanitize(propertyName)).append("';");
				ResultSet rs = query(q.toString());

				while ( rs.next() )
					val = rs.getString("value");

				rs.close();

				// if the value isn't empty, update the local and return it
				if ( !val.equals("") ) {
					setProperty(propertyName, val);
					return val;
				}
			}
		}

		// Property doesn't exist, or is set to "", so just set to default value
		// and return that.
		setProperty(propertyName, defaultValue);
		return defaultValue;
	}

	/**
	 * Given a propertyName, set its value to the given newValue.
	 * @param propertyName
	 * @param newValue
	 * @throws SQLException 
	 */
	public void setProperty(String propertyName, String newValue) 
	throws SQLException {
		// first deal with the SQL bit
		StringBuilder q = new StringBuilder("");
		if ( checkPropertyExists(propertyName) ) {
			// update DB
			q.append("UPDATE DEVICE_PROPERTIES SET value='");
			q.append(sanitize(newValue)).append("' WHERE property='");
			q.append(sanitize(propertyName)).append("';");
		}
		else {
			// add to DB
			q.append("INSERT INTO DEVICE_PROPERTIES(property,value) VALUES('");
			q.append(sanitize(propertyName)).append("', '");
			q.append(sanitize(newValue)).append("');");
		}

		// commit to DB
		int r = update(q.toString());

		// now update the local properties map
		if ( properties.containsKey(propertyName) )
			properties.replace(propertyName, newValue);
		else
			properties.put(propertyName, newValue);
	}

	/**
	 * Dump data in a ResultSet to console.
	 * @param rs
	 * @throws SQLException
	 */
	public void dump(ResultSet rs) throws SQLException {
		ResultSetMetaData meta   = rs.getMetaData();
		int               colmax = meta.getColumnCount();
		int               i;
		Object            o = null;
	
		System.out.println(name + " :: DATA DUMP -> ");
	
		String n = "";
		for (int j = 0; j < colmax; j++) {
			n = meta.getColumnLabel(j+1);
			System.out.print(n + " :: ");
		}
	
		System.out.println(" ");
	
		while ( rs.next() ) {
			for (i = 0; i < colmax; i++) {
				o = rs.getObject(i + 1);
				if ( o != null ) 
					System.out.print(o.toString());
				System.out.print(" :: ");
			}
	
			System.out.println(" ");
		}
	
		rs.close();
	}

	/**
	 * Get the extension of a file, if any.
	 * @param s
	 * @return
	 */
	public static String getExtension(String s) {	
		int start = s.indexOf(".");
	
		if ( s.length() <= 1 )
			return s;
		
		String sub = s.substring(start+1); 
		if ( sub.indexOf(".") != -1 )
			return Device.getExtension(sub);
	
		return sub;
	}

	/**
	 * Get the last folder in a string of folder names, seperated by 
	 * File.pathSeparator.
	 * @param s
	 * @return
	 */
	public static String getFolderName(String s) {
		int start = s.indexOf(File.separator);
	
		if ( s.length() <= 1 )
			return s;
		
		String sub = s.substring(start+1); 
		if ( sub.indexOf(File.separator) != -1 )
			return Device.getFolderName(sub);
	
		return sub;
	}

	/**
	 * Set default Device properties upon creation
	 * @throws SQLException
	 */
	private void setDefaultProperties() throws SQLException {
		// Populate Properties
		getProperty("icon", "icon01.png");
	}

	/**
	 * True iff the property exists in the database.
	 * @param propertyName
	 * @param value
	 * @return
	 * @throws SQLException 
	 */
	private boolean checkPropertyExists(String propertyName) 
	throws SQLException {
		StringBuilder q = new StringBuilder();
		q.append("SELECT * FROM DEVICE_PROPERTIES WHERE property='");
		q.append(sanitize(propertyName)).append("';");
		ResultSet rs = query(q.toString());

		System.out.println("Looking for property: " + propertyName);
		String e = "";
		while ( rs.next() ) {
			e = rs.getString("property");
			System.out.println("\t -> " + e);
			if ( e.equals(propertyName) ) {
				System.out.println("\t ->>>>> TRUE");
				rs.close();
				return true;
			}
		}

		System.out.println("\t -> False");
		rs.close();
		return false;
	}

	/**
	 * Populate all tables in the Database if they do not already exist.
	 * @throws SQLException
	 */
	private void populateTables() throws SQLException {
		// TODO ugh...so need a better way to do this table business
		tables.put("FILE_SYSTEM_DATA", 
				"CREATE CACHED TABLE FILE_SYSTEM_DATA ( " +
					"property VARCHAR(64), " +
					"value OBJECT);"
				);
		
		tables.put("DEVICE_ITEMS", 
				"CREATE CACHED TABLE DEVICE_ITEMS ( " +
					"itemID INTEGER IDENTITY, " +
					"value OBJECT);"
				);

		tables.put("DEVICE_PROPERTIES", 
				"CREATE CACHED TABLE DEVICE_PROPERTIES ( " +
					"property VARCHAR(256), " +
					"value VARCHAR(2048));"
				);

		// First, lets get a list of any existing tables	
		String getTables = "SELECT * FROM information_schema.system_tables WHERE TABLE_SCHEM = 'PUBLIC';";
		ResultSet rs = query(getTables);

		// Check all TABLE_NAME's in the system_tables, make sure every table
		// name specified in the field tables is present.
		String e = "";
		while ( rs.next() ) {
			e = rs.getString("TABLE_NAME");
			if ( tables.containsKey(e) )
				tables.remove(e);
		}

		if ( tables.size() > 0 ) {
			Enumeration<String> tableEnum = tables.elements();
			String t = "";
			while ( tableEnum.hasMoreElements() ) {
				t = tableEnum.nextElement();
				update(t);
				
				// initialize file system data
				if ( t.equals(tables.get("FILE_SYSTEM_DATA")) ) {
//					System.out.println("Init FSData: " + name);
//					update("INSERT INTO FILE_SYSTEM_DATA(property,value) VALUES('FS', '');");
//					update("INSERT INTO FILE_SYSTEM_DATA(property,value) VALUES('IDG', '');");
				}
			}
		}
		rs.close();
	}

	/**
	 * Run the given update-type expression on the open database.
	 * @param expr the expression to execute
	 * @return the resulting int. 
	 * @throws SQLException
	 */
	private int update(String expr) throws SQLException {
		Statement st = conn.createStatement();
		return st.executeUpdate(expr);
	}

	/**
	 * Run the given update-type expression on the open database.
	 * @param expr the expression to execute
	 * @return the resulting int. 
	 * @throws SQLException
	 */
	private int update(StringBuilder expr) throws SQLException {
		return update(expr.toString());
	}

	/**
	 * Run the given query and return the ResultSet
	 * @param expression the expression to run
	 * @return a ResultSet
	 * @throws SQLException
	 */
	private synchronized ResultSet query(String expression) 
	throws SQLException {
		Statement st = conn.createStatement();
		return st.executeQuery(expression);
	}

	private String sanitize(String s) {
		String n = s;
		n = n.replace('\'', '"');
		return n;
	}
}
